ποΈGitΠ―ΡΠ°ποΈ
Node / meshtastic / Meshtastic-Android / files / feature / connections / src / commonMain / kotlin / org / meshtastic / feature / connections / ui / ConnectionsScreen.kt
Displaying Raw β’ Download
feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 37.84 KB
T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.connections.ui
Tff7b72import T7ee787androidx.compose.animation.AnimatedContent
Tff7b72import T7ee787androidx.compose.animation.animateContentSize
Tff7b72import T7ee787androidx.compose.animation.fadeIn
Tff7b72import T7ee787androidx.compose.animation.fadeOut
Tff7b72import T7ee787androidx.compose.animation.togetherWith
Tff7b72import T7ee787androidx.compose.foundation.layout.Arrangement
Tff7b72import T7ee787androidx.compose.foundation.layout.Box
Tff7b72import T7ee787androidx.compose.foundation.layout.Column
Tff7b72import T7ee787androidx.compose.foundation.layout.Row
Tff7b72import T7ee787androidx.compose.foundation.layout.Spacer
Tff7b72import T7ee787androidx.compose.foundation.layout.fillMaxSize
Tff7b72import T7ee787androidx.compose.foundation.layout.fillMaxWidth
Tff7b72import T7ee787androidx.compose.foundation.layout.height
Tff7b72import T7ee787androidx.compose.foundation.layout.heightIn
Tff7b72import T7ee787androidx.compose.foundation.layout.padding
Tff7b72import T7ee787androidx.compose.foundation.layout.size
Tff7b72import T7ee787androidx.compose.foundation.layout.width
Tff7b72import T7ee787androidx.compose.material3.Button
Tff7b72import T7ee787androidx.compose.material3.Card
Tff7b72import T7ee787androidx.compose.material3.CardDefaults
Tff7b72import T7ee787androidx.compose.material3.ExperimentalMaterial3Api
Tff7b72import T7ee787androidx.compose.material3.Icon
Tff7b72import T7ee787androidx.compose.material3.MaterialTheme
Tff7b72import T7ee787androidx.compose.material3.Scaffold
Tff7b72import T7ee787androidx.compose.material3.Text
Tff7b72import T7ee787androidx.compose.runtime.Composable
Tff7b72import T7ee787androidx.compose.runtime.LaunchedEffect
Tff7b72import T7ee787androidx.compose.runtime.getValue
Tff7b72import T7ee787androidx.compose.runtime.mutableStateOf
Tff7b72import T7ee787androidx.compose.runtime.remember
Tff7b72import T7ee787androidx.compose.runtime.setValue
Tff7b72import T7ee787androidx.compose.ui.Alignment
Tff7b72import T7ee787androidx.compose.ui.Modifier
Tff7b72import T7ee787androidx.compose.ui.platform.LocalUriHandler
Tff7b72import T7ee787androidx.compose.ui.text.style.TextAlign
Tff7b72import T7ee787androidx.compose.ui.unit.dp
Tff7b72import T7ee787androidx.lifecycle.compose.LifecycleStartEffect
Tff7b72import T7ee787androidx.lifecycle.compose.collectAsStateWithLifecycle
Tff7b72import T7ee787org.jetbrains.compose.resources.stringResource
Tff7b72import T7ee787org.koin.compose.viewmodel.koinViewModel
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DeviceType
Tff7b72import T7ee787org.meshtastic.core.model.FirmwareUpdateDestination
Tff7b72import T7ee787org.meshtastic.core.model.FirmwareUpdateNotice
Tff7b72import T7ee787org.meshtastic.core.model.InterfaceId
Tff7b72import T7ee787org.meshtastic.core.navigation.FirmwareRoute
Tff7b72import T7ee787org.meshtastic.core.navigation.Route
Tff7b72import T7ee787org.meshtastic.core.navigation.SettingsRoute
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.bluetooth_disabled
Tff7b72import T7ee787org.meshtastic.core.resources.connections
Tff7b72import T7ee787org.meshtastic.core.resources.disconnect
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_event_ended_banner
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_event_ended_button
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_recovery_banner
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_recovery_button
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_recovery_dismiss
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_update_available
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_update_notification_android
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_update_notification_flasher
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_update_open
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_update_open_flasher
Tff7b72import T7ee787org.meshtastic.core.resources.firmware_version
Tff7b72import T7ee787org.meshtastic.core.resources.no_device_selected
Tff7b72import T7ee787org.meshtastic.core.resources.open_bluetooth_settings
Tff7b72import T7ee787org.meshtastic.core.resources.open_wifi_settings
Tff7b72import T7ee787org.meshtastic.core.resources.rssi
Tff7b72import T7ee787org.meshtastic.core.resources.set_your_region
Tff7b72import T7ee787org.meshtastic.core.resources.unknown
Tff7b72import T7ee787org.meshtastic.core.resources.unknown_device
Tff7b72import T7ee787org.meshtastic.core.resources.wifi_unavailable
Tff7b72import T7ee787org.meshtastic.core.ui.component.AdaptiveTwoPane
Tff7b72import T7ee787org.meshtastic.core.ui.component.ListItem
Tff7b72import T7ee787org.meshtastic.core.ui.component.MainAppBar
Tff7b72import T7ee787org.meshtastic.core.ui.component.RecoveryCard
Tff7b72import T7ee787org.meshtastic.core.ui.icon.Bluetooth
Tff7b72import T7ee787org.meshtastic.core.ui.icon.Language
Tff7b72import T7ee787org.meshtastic.core.ui.icon.MeshtasticIcons
Tff7b72import T7ee787org.meshtastic.core.ui.icon.NoDevice
Tff7b72import T7ee787org.meshtastic.core.ui.icon.SystemUpdate
Tff7b72import T7ee787org.meshtastic.core.ui.util.LocalEventBranding
Tff7b72import T7ee787org.meshtastic.core.ui.util.PermissionStatus
Tff7b72import T7ee787org.meshtastic.core.ui.util.hasEnded
Tff7b72import T7ee787org.meshtastic.core.ui.util.isBluetoothDisabled
Tff7b72import T7ee787org.meshtastic.core.ui.util.isWifiUnavailable
Tff7b72import T7ee787org.meshtastic.core.ui.util.rememberBluetoothPermissionState
Tff7b72import T7ee787org.meshtastic.core.ui.util.rememberLocalNetworkPermissionState
Tff7b72import T7ee787org.meshtastic.core.ui.util.rememberOpenBluetoothSettings
Tff7b72import T7ee787org.meshtastic.core.ui.util.rememberOpenWifiSettings
Tff7b72import T7ee787org.meshtastic.core.ui.util.shouldShowWifiUnavailableBanner
Tff7b72import T7ee787org.meshtastic.core.ui.viewmodel.ConnectionStatus
Tff7b72import T7ee787org.meshtastic.core.ui.viewmodel.ConnectionsViewModel
Tff7b72import T7ee787org.meshtastic.feature.connections.NO_DEVICE_SELECTED
Tff7b72import T7ee787org.meshtastic.feature.connections.ScannerViewModel
Tff7b72import T7ee787org.meshtastic.feature.connections.model.DeviceListEntry
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.ConnectingDeviceInfo
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.CurrentlyConnectedInfo
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.CurrentlyConnectedText
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.DeviceList
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.EventFirmwareCard
Tff7b72import T7ee787org.meshtastic.feature.connections.ui.components.TransportSelector
T8b949e/**
* Fixed minimum height for the "connected device" card at the top of the Connections screen. Shared across the three UI
* states (NO_DEVICE, CONNECTING, CONNECTED_WITH_NODE) so the card never collapses or jumps size between state
* transitions. Sized to comfortably fit the CONNECTED state (battery/RSSI row + node row + disconnect button).
*/
Tff7b72private Tff7b72val Te6edf3CardMinHeight Tff7b72= T79c0ff1T79c0ff0T79c0ff0.Te6edf3dp
T8b949e/** Composable screen for managing device connections (BLE, TCP, USB). It displays connection status. */
Tf0883e@OptInTb4b4b4(Te6edf3ExperimentalMaterial3ApiTff7b72::Te6edf3classTb4b4b4)
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffLongMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffModifierMissingTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffComposableParamOrderTa5d6ff"Tb4b4b4)
Tf0883e@Composable
Tff7b72fun Td2a8ffConnectionsScreenTb4b4b4(
Te6edf3connectionsViewModelTb4b4b4: Te6edf3ConnectionsViewModel Tff7b72= Te6edf3koinViewModelTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3scanModelTb4b4b4: Te6edf3ScannerViewModel Tff7b72= Te6edf3koinViewModelTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3onClickNodeChipTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3onNavigateToNodeDetailsTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3onConfigNavigateTb4b4b4: Tb4b4b4(Te6edf3RouteTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3connectionProgress Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3connectionProgressTextTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connectionStatus Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3connectionStatusTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3connectionState Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3connectionStateTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3ourNode Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3ourNodeForDisplayTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3firmwareUpdateNotice Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3firmwareUpdateNoticeTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3regionUnset Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3regionUnsetTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3sessionAuthorized Tff7b72by Te6edf3connectionsViewModelTb4b4b4.Te6edf3sessionAuthorizedTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3selectedDevice Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3selectedNotNullFlowTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3persistedDeviceName Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3persistedDeviceNameTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3pendingRecovery Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3pendingRecoveryTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3bleDevices Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3bleDevicesForUiTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3discoveredTcpDevices Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3discoveredTcpDevicesForUiTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3recentTcpDevices Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3recentTcpDevicesForUiTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3usbDevices Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3usbDevicesForUiTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3isBleScanning Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3isBleScanningTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3isNetworkScanning Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3isNetworkScanningTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3activeTransport Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3activeTransportTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3bleAutoScan Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3bleAutoScanTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3networkAutoScan Tff7b72by Te6edf3scanModelTb4b4b4.Te6edf3networkAutoScanTb4b4b4.Te6edf3collectAsStateWithLifecycleTb4b4b4(Tb4b4b4)
T8b949e// Android 17 (API 37) gates NSD/mDNS behind ACCESS_LOCAL_NETWORK. Without this prompt the platform falls back to
T8b949e// the system "Choose a device to connect" picker on every discoverServices() call. The reactive state lets the
T8b949e// network-scan toggle request in-context and route a permanent denial to settings.
Tff7b72val Te6edf3localNetworkPermission Tff7b72= Te6edf3rememberLocalNetworkPermissionStateTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3bluetoothPermission Tff7b72= Te6edf3rememberBluetoothPermissionStateTb4b4b4(Tb4b4b4)
T8b949e// ACCESS_LOCAL_NETWORK gates the socket, not just discovery β a blocked local TCP connect times out rather than
T8b949e// failing fast β but a TCP address says nothing about locality: public-IP/port-forward/VPN radios need no
T8b949e// permission at all. Policy (see LocalNetworkGateAction): prompt when possible, warn when not, never block.
T8b949e// A connect issued while the prompt is up is stashed with the status it saw; the LaunchedEffect below resolves it
T8b949e// on the status transition the request produces β grant runs it, a denial warns and runs it anyway.
Tff7b72var Te6edf3pendingTcpConnect Tff7b72by Te6edf3remember Tb4b4b4{ Te6edf3mutableStateOfTff7b72<Te6edf3PairTff7b72<Te6edf3PermissionStatusTb4b4b4, Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTff7b72>Tff7b72?Tff7b72>Tb4b4b4(Tff7b72nullTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3gateTcpConnectTb4b4b4: Tb4b4b4(Te6edf3connectTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4) Tff7b72-Tff7b72> Tffa657Unit Tff7b72= Tb4b4b4{ Te6edf3connect Tff7b72-Tff7b72>
Tff7b72when Tb4b4b4(Te6edf3localNetworkGateActionTb4b4b4(Te6edf3localNetworkPermissionTb4b4b4.Te6edf3statusTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3LocalNetworkGateActionTb4b4b4.Te6edf3PROCEED Tff7b72-Tff7b72> Te6edf3connectTb4b4b4(Tb4b4b4)
Te6edf3LocalNetworkGateActionTb4b4b4.Te6edf3REQUEST_PERMISSION Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3pendingTcpConnect Tff7b72= Te6edf3localNetworkPermissionTb4b4b4.Te6edf3status Te6edf3to Te6edf3connect
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3requestTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3LocalNetworkGateActionTb4b4b4.Te6edf3PROCEED_WITH_WARNING Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3warnLocalNetworkPermissionDeniedTb4b4b4(Tb4b4b4)
Te6edf3connectTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Te6edf3LaunchedEffectTb4b4b4(Te6edf3localNetworkPermissionTb4b4b4.Te6edf3statusTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3pending Tff7b72= Te6edf3pendingTcpConnect Tff7b72?: Tff7b72returnTf0883e@LaunchedEffect
Tff7b72when Tb4b4b4(Te6edf3resolvePendingTcpConnectTb4b4b4(Te6edf3stashedStatus Tff7b72= Te6edf3pendingTb4b4b4.Te6edf3firstTb4b4b4, Te6edf3currentStatus Tff7b72= Te6edf3localNetworkPermissionTb4b4b4.Te6edf3statusTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3PendingTcpConnectResolutionTb4b4b4.Te6edf3CONNECT Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3pendingTcpConnect Tff7b72= Tff7b72null
Te6edf3pendingTb4b4b4.Te6edf3secondTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3PendingTcpConnectResolutionTb4b4b4.Te6edf3CONNECT_WITH_WARNING Tff7b72-Tff7b72> Tb4b4b4{
Te6edf3pendingTcpConnect Tff7b72= Tff7b72null
Te6edf3scanModelTb4b4b4.Te6edf3warnLocalNetworkPermissionDeniedTb4b4b4(Tb4b4b4)
Te6edf3pendingTb4b4b4.Te6edf3secondTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3PendingTcpConnectResolutionTb4b4b4.Te6edf3KEEP_WAITING Tff7b72-Tff7b72> Tffa657Unit
Tb4b4b4}
Tb4b4b4}
T8b949e// Adapter-state, distinct from permission state: a permission can be granted while Bluetooth is off or the device
T8b949e// is off Wi-Fi. Detected separately so the UI can route to the adapter's settings rather than re-prompting.
Tff7b72val Te6edf3bluetoothDisabled Tff7b72= Te6edf3isBluetoothDisabledTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3wifiUnavailable Tff7b72= Te6edf3isWifiUnavailableTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3openBluetoothSettings Tff7b72= Te6edf3rememberOpenBluetoothSettingsTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3openWifiSettings Tff7b72= Te6edf3rememberOpenWifiSettingsTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3uriHandler Tff7b72= Te6edf3LocalUriHandlerTb4b4b4.Te6edf3current
T8b949e// Auto-start BLE discovery when the screen is visible (lifecycle β₯ STARTED) and the user has previously opted in.
T8b949e// ScannerViewModel skips screen-entry discovery when a selected device can reconnect through the transport's
T8b949e// fresh-advertisement scan. LifecycleStartEffect stops scanning on ON_STOP (app backgrounded) and restarts on
T8b949e// ON_START β preventing continuous background BLE radio usage that drains the battery.
T8b949e// Keyed on the active pane so a persisted TCP/USB pane loaded after first composition disposes this effect and
T8b949e// stops an initially eligible BLE scan. The toggle handler starts/stops scans directly; this effect owns lifecycle
T8b949e// cleanup, while the LaunchedEffect below handles async preference loading without disposing the lifecycle owner.
Te6edf3LifecycleStartEffectTb4b4b4(Te6edf3activeTransportTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3BLE Tff7b72&Tff7b72& Te6edf3bleAutoScan Tff7b72&Tff7b72& Tff7b72!Te6edf3isBleScanningTb4b4b4) Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3onStopOrDispose Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3stopBleScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Te6edf3LaunchedEffectTb4b4b4(Te6edf3activeTransportTb4b4b4, Te6edf3bleAutoScanTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3BLE Tff7b72&Tff7b72& Te6edf3bleAutoScan Tff7b72&Tff7b72& Tff7b72!Te6edf3isBleScanningTb4b4b4) Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3startBleAutoScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e// Keyed on active pane and permission status so the lifecycle owner re-fires when the user grants local-network
T8b949e// permission or the persisted Network pane loads. The separate LaunchedEffect handles later pref arrival without a
T8b949e// dispose+restart cycle on manual scan preference writes.
Te6edf3LifecycleStartEffectTb4b4b4(Te6edf3activeTransportTb4b4b4, Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGrantedTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(
Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3TCP Tff7b72&Tff7b72&
Te6edf3networkAutoScan Tff7b72&Tff7b72&
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGranted Tff7b72&Tff7b72&
Tff7b72!Te6edf3isNetworkScanning
Tb4b4b4) Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3startNetworkAutoScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Te6edf3onStopOrDispose Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3stopNetworkScanTb4b4b4(Tb4b4b4) Tb4b4b4}
Tb4b4b4}
Te6edf3LaunchedEffectTb4b4b4(Te6edf3activeTransportTb4b4b4, Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGrantedTb4b4b4, Te6edf3networkAutoScanTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(
Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3TCP Tff7b72&Tff7b72&
Te6edf3networkAutoScan Tff7b72&Tff7b72&
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGranted Tff7b72&Tff7b72&
Tff7b72!Te6edf3isNetworkScanning
Tb4b4b4) Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3startNetworkAutoScanTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e// Work around CMP-6615 in Compose Multiplatform 1.11.1: Android stringResource enters a blocking resource state.
T8b949e// Keep these stable slots outside AnimatedContent so connection-state transitions do not re-enter resource loading
T8b949e// while the main thread is applying an animation frame.
Tff7b72val Te6edf3firmwareVersion Tff7b72=
Te6edf3ourNode
Tff7b72?.Te6edf3metadata
Tff7b72?.Te6edf3firmware_version
Tff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_versionTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3currentlyConnectedText Tff7b72=
Te6edf3CurrentlyConnectedTextTb4b4b4(
Te6edf3unknownLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3unknownTb4b4b4)Tb4b4b4,
Te6edf3rssiLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3rssiTb4b4b4)Tb4b4b4,
Te6edf3disconnectLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3disconnectTb4b4b4)Tb4b4b4,
Te6edf3firmwareVersion Tff7b72= Te6edf3firmwareVersionTb4b4b4,
Tb4b4b4)
Te6edf3ScaffoldTb4b4b4(
Te6edf3topBar Tff7b72= Tb4b4b4{
Te6edf3MainAppBarTb4b4b4(
Te6edf3title Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3connectionsTb4b4b4)Tb4b4b4,
Te6edf3ourNode Tff7b72= Te6edf3ourNodeTb4b4b4,
Te6edf3showNodeChip Tff7b72= Te6edf3ourNode Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3connectionState Tff7b72is Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4,
Te6edf3canNavigateUp Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3onNavigateUp Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3actions Tff7b72= Tb4b4b4{Tb4b4b4}Tb4b4b4,
Te6edf3onClickChip Tff7b72= Tb4b4b4{ Te6edf3onClickNodeChipTb4b4b4(Tffa657itTb4b4b4.Te6edf3numTb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}Tb4b4b4,
Tb4b4b4) Tb4b4b4{ Te6edf3paddingValues Tff7b72-Tff7b72>
Te6edf3BoxTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxSizeTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3ColumnTb4b4b4(
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxSizeTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3paddingValuesTb4b4b4)Tb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3horizontal Tff7b72= T79c0ff1T79c0ff6.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3verticalArrangement Tff7b72= Te6edf3ArrangementTb4b4b4.Te6edf3spacedByTb4b4b4(T79c0ff1T79c0ff6.Te6edf3dpTb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3heightTb4b4b4(T79c0ff4.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3AdaptiveTwoPaneTb4b4b4(
Te6edf3first Tff7b72= Tb4b4b4{
Tff7b72val Te6edf3uiState Tff7b72=
Tff7b72when Tb4b4b4{
Te6edf3connectionState Tff7b72is Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected Tff7b72&Tff7b72& Te6edf3ourNode Tff7b72!Tff7b72= Tff7b72null Tff7b72-Tff7b72>
Te6edf3ConnectionUiStateTb4b4b4.Te6edf3CONNECTED_WITH_NODE
Te6edf3connectionState Tff7b72is Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected Tff7b72|Tff7b72|
Te6edf3connectionState Tff7b72=Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connecting Tff7b72|Tff7b72|
Te6edf3selectedDevice Tff7b72!Tff7b72= Te6edf3NO_DEVICE_SELECTED Tff7b72-Tff7b72> Te6edf3ConnectionUiStateTb4b4b4.Te6edf3CONNECTING
Tff7b72else Tff7b72-Tff7b72> Te6edf3ConnectionUiStateTb4b4b4.Te6edf3NO_DEVICE
Tb4b4b4}
T8b949e// ββ Connected Device slot ββ
T8b949e// A single Card shell hosts all three states. `animateContentSize` smooths any
T8b949e// height changes, while `heightIn(min = CardMinHeight)` reserves a stable floor so
T8b949e// the card never collapses between states.
Te6edf3CardTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3animateContentSizeTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3AnimatedContentTb4b4b4(
Te6edf3targetState Tff7b72= Te6edf3uiStateTb4b4b4,
Te6edf3label Tff7b72= Ta5d6ff"Ta5d6ffconnection_stateTa5d6ff"Tb4b4b4,
Te6edf3transitionSpec Tff7b72= Tb4b4b4{ Te6edf3fadeInTb4b4b4(Tb4b4b4) Te6edf3togetherWith Te6edf3fadeOutTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4{ Te6edf3state Tff7b72-Tff7b72>
Te6edf3BoxTb4b4b4(
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3heightInTb4b4b4(Te6edf3min Tff7b72= Te6edf3CardMinHeightTb4b4b4)Tb4b4b4,
Te6edf3contentAlignment Tff7b72= Te6edf3AlignmentTb4b4b4.Te6edf3CenterTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72when Tb4b4b4(Te6edf3stateTb4b4b4) Tb4b4b4{
Te6edf3ConnectionUiStateTb4b4b4.Te6edf3CONNECTED_WITH_NODE Tff7b72-Tff7b72>
Te6edf3ConnectedDeviceContentTb4b4b4(
Te6edf3ourNode Tff7b72= Te6edf3ourNodeTb4b4b4,
Te6edf3selectedDevice Tff7b72= Te6edf3selectedDeviceTb4b4b4,
Te6edf3bleDevices Tff7b72= Te6edf3bleDevicesTb4b4b4,
Te6edf3text Tff7b72= Te6edf3currentlyConnectedTextTb4b4b4,
Te6edf3onNavigateToNodeDetails Tff7b72= Te6edf3onNavigateToNodeDetailsTb4b4b4,
Te6edf3onClickDisconnect Tff7b72= Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Te6edf3ConnectionUiStateTb4b4b4.Te6edf3CONNECTING Tff7b72-Tff7b72>
Te6edf3ConnectingDeviceContentTb4b4b4(
Te6edf3selectedDevice Tff7b72= Te6edf3selectedDeviceTb4b4b4,
Te6edf3persistedDeviceName Tff7b72= Te6edf3persistedDeviceNameTb4b4b4,
Te6edf3bleDevices Tff7b72= Te6edf3bleDevicesTb4b4b4,
Te6edf3discoveredTcpDevices Tff7b72= Te6edf3discoveredTcpDevicesTb4b4b4,
Te6edf3recentTcpDevices Tff7b72= Te6edf3recentTcpDevicesTb4b4b4,
Te6edf3usbDevices Tff7b72= Te6edf3usbDevicesTb4b4b4,
Te6edf3connectionStatus Tff7b72= Te6edf3connectionStatusTb4b4b4,
Te6edf3connectionProgress Tff7b72= Te6edf3connectionProgressTb4b4b4,
Te6edf3onClickDisconnect Tff7b72= Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3disconnectTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72else Tff7b72-Tff7b72> Te6edf3NoDeviceContentTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e// Event firmware is reported here rather than by swapping the app-bar logo: the Meshtastic
T8b949e// identity stays put, and the edition reads as one more fact about the connected device.
T8b949e// LocalEventBranding is only populated while connected to event firmware, so the card comes
T8b949e// and goes with the device. Hidden once the event is over β the ended-event card below takes
T8b949e// over from here, and celebrating an event that has passed would undercut its nudge.
Te6edf3LocalEventBrandingTb4b4b4.Te6edf3current
Tff7b72?.Te6edf3takeIf Tb4b4b4{ Tff7b72!Tffa657itTb4b4b4.Te6edf3hasEndedTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3edition Tff7b72-Tff7b72>
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3heightTb4b4b4(T79c0ff8.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3EventFirmwareCardTb4b4b4(Te6edf3edition Tff7b72= Te6edf3editionTb4b4b4)
Tb4b4b4}
Te6edf3firmwareUpdateNoticeTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3notice Tff7b72-Tff7b72>
Te6edf3FirmwareUpdateNoticeCardTb4b4b4(
Te6edf3notice Tff7b72= Te6edf3noticeTb4b4b4,
Te6edf3onAction Tff7b72= Tb4b4b4{
Tff7b72when Tb4b4b4(Te6edf3noticeTb4b4b4.Te6edf3destinationTb4b4b4) Tb4b4b4{
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3AndroidUpdate Tff7b72-Tff7b72>
Te6edf3onConfigNavigateTb4b4b4(Te6edf3FirmwareRouteTb4b4b4.Te6edf3FirmwareUpdateTb4b4b4)
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3MeshtasticFlasher Tff7b72-Tff7b72>
Te6edf3uriHandlerTb4b4b4.Te6edf3openUriTb4b4b4(Ta5d6ff"Ta5d6ffhttps://flasher.meshtastic.orgTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e// A device stranded in bootloader mode by an interrupted update can be re-flashed without
T8b949e// reconnecting first. Shown only while disconnected so the Firmware screen enters its recovery
T8b949e// path (it uses the live connection when connected); cleared automatically once the device
T8b949e// returns on its own.
Te6edf3pendingRecovery
Tff7b72?.Te6edf3takeIf Tb4b4b4{ Te6edf3connectionState Tff7b72!is Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3recovery Tff7b72-Tff7b72>
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3heightTb4b4b4(T79c0ff8.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3RecoveryCardTb4b4b4(
Te6edf3message Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_recovery_bannerTb4b4b4, Te6edf3recoveryTb4b4b4.Te6edf3deviceNameTb4b4b4)Tb4b4b4,
Te6edf3actionLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_recovery_buttonTb4b4b4)Tb4b4b4,
Te6edf3onAction Tff7b72= Tb4b4b4{ Te6edf3onConfigNavigateTb4b4b4(Te6edf3FirmwareRouteTb4b4b4.Te6edf3FirmwareUpdateTb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3actionIcon Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3BluetoothTb4b4b4,
T8b949e// Let the user dismiss a recovery that can't succeed (e.g. an unflashable stock
T8b949e// bootloader) so it doesn't nag forever; it otherwise only clears on
T8b949e// reconnect/success.
Te6edf3onDismiss Tff7b72= Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3dismissRecoveryTb4b4b4(Tb4b4b4) Tb4b4b4}Tb4b4b4,
Te6edf3dismissContentDescription Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_recovery_dismissTb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e// Once an event is over, nudge users still on that event's firmware back to standard
T8b949e// firmware. Driven purely by the metadata end date (LocalEventBranding is only populated
T8b949e// while connected to event firmware), so it appears whenever an ended-event device is
T8b949e// connected and disappears on its own once the device is re-flashed to vanilla. Not
T8b949e// dismissable β it stays until the underlying condition is actually resolved.
Te6edf3LocalEventBrandingTb4b4b4.Te6edf3current
Tff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3hasEndedTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3endedEvent Tff7b72-Tff7b72>
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3heightTb4b4b4(T79c0ff8.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3RecoveryCardTb4b4b4(
Te6edf3message Tff7b72=
Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_event_ended_bannerTb4b4b4, Te6edf3endedEventTb4b4b4.Te6edf3displayNameTb4b4b4)Tb4b4b4,
Te6edf3actionLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_event_ended_buttonTb4b4b4)Tb4b4b4,
Te6edf3onAction Tff7b72= Tb4b4b4{ Te6edf3onConfigNavigateTb4b4b4(Te6edf3FirmwareRouteTb4b4b4.Te6edf3FirmwareUpdateTb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e// Region warning sits outside the animated card so it does not affect the
T8b949e// CONNECTED β CONNECTING β NO_DEVICE size transition.
Tff7b72val Te6edf3isPhysicalDevice Tff7b72=
Te6edf3selectedDevice Tff7b72!Tff7b72= Te6edf3InterfaceIdTb4b4b4.Te6edf3MOCKTb4b4b4.Te6edf3idTb4b4b4.Te6edf3toStringTb4b4b4(Tb4b4b4) Tff7b72&Tff7b72&
Te6edf3selectedDevice Tff7b72!Tff7b72= Te6edf3InterfaceIdTb4b4b4.Te6edf3REPLAYTb4b4b4.Te6edf3idTb4b4b4.Te6edf3toStringTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(
Te6edf3uiState Tff7b72=Tff7b72= Te6edf3ConnectionUiStateTb4b4b4.Te6edf3CONNECTED_WITH_NODE Tff7b72&Tff7b72&
Te6edf3regionUnset Tff7b72&Tff7b72&
Te6edf3sessionAuthorized Tff7b72&Tff7b72&
Te6edf3isPhysicalDevice
Tb4b4b4) Tb4b4b4{
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3heightTb4b4b4(T79c0ff8.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3CardTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3ListItemTb4b4b4(
Te6edf3leadingIcon Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3LanguageTb4b4b4,
Te6edf3text Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3set_your_regionTb4b4b4)Tb4b4b4,
T8b949e// Navigate straight to the LoRa screen: it re-reads the route on entry and
T8b949e// renders from the connect-time snapshot meanwhile, so pre-fetching behind a
T8b949e// progress dialog here bought nothing and could strand the user on an empty
T8b949e// dialog when the read completed before the dialog observed it.
Te6edf3onClick Tff7b72= Tb4b4b4{ Te6edf3onConfigNavigateTb4b4b4(Te6edf3SettingsRouteTb4b4b4.Te6edf3LoRaTb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e// Transport selector sits between the connection card and device list; it controls only the
T8b949e// visible discovery pane, not the globally selected/connected device shown above.
Te6edf3TransportSelectorTb4b4b4(
Te6edf3activeTransport Tff7b72= Te6edf3activeTransportTb4b4b4,
Te6edf3onSelectTransport Tff7b72= Te6edf3scanModelTff7b72::Te6edf3selectTransportTb4b4b4,
Tb4b4b4)
T8b949e// Adapter-off hints: shown only when the relevant permission is granted but the radio/network
T8b949e// is unavailable, so they don't overlap the permission-recovery flow on the scan toggles.
T8b949e// The WiFi-unavailable banner only renders while a network scan is actively running β
T8b949e// discovery is the only moment the user needs to know WiFi is missing. The auto-scan case is
T8b949e// covered because `isNetworkScanning` is true during auto-scan regardless of pane state.
Tff7b72if Tb4b4b4(Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3BLE Tff7b72&Tff7b72& Te6edf3bluetoothPermissionTb4b4b4.Te6edf3isGranted Tff7b72&Tff7b72& Te6edf3bluetoothDisabledTb4b4b4) Tb4b4b4{
Te6edf3RecoveryCardTb4b4b4(
Te6edf3message Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3bluetooth_disabledTb4b4b4)Tb4b4b4,
Te6edf3actionLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3open_bluetooth_settingsTb4b4b4)Tb4b4b4,
Te6edf3onAction Tff7b72= Te6edf3openBluetoothSettingsTb4b4b4,
Te6edf3actionIcon Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3BluetoothTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(
Te6edf3activeTransport Tff7b72=Tff7b72= Te6edf3DeviceTypeTb4b4b4.Te6edf3TCP Tff7b72&Tff7b72&
Te6edf3shouldShowWifiUnavailableBannerTb4b4b4(
Te6edf3isNetworkScanning Tff7b72= Te6edf3isNetworkScanningTb4b4b4,
Te6edf3localNetworkPermissionGranted Tff7b72= Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGrantedTb4b4b4,
Te6edf3wifiUnavailable Tff7b72= Te6edf3wifiUnavailableTb4b4b4,
Te6edf3discoveredTcpDevicesEmpty Tff7b72= Te6edf3discoveredTcpDevicesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4) Tb4b4b4{
Te6edf3RecoveryCardTb4b4b4(
Te6edf3message Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3wifi_unavailableTb4b4b4)Tb4b4b4,
Te6edf3actionLabel Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3open_wifi_settingsTb4b4b4)Tb4b4b4,
Te6edf3onAction Tff7b72= Te6edf3openWifiSettingsTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3second Tff7b72= Tb4b4b4{
T8b949e// ββ Unified device list ββ
Te6edf3BoxTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3weightTb4b4b4(T79c0ff1fTb4b4b4)Tb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3DeviceListTb4b4b4(
Te6edf3connectionState Tff7b72= Te6edf3connectionStateTb4b4b4,
Te6edf3selectedDevice Tff7b72= Te6edf3selectedDeviceTb4b4b4,
Te6edf3bleDevices Tff7b72= Te6edf3bleDevicesTb4b4b4,
Te6edf3usbDevices Tff7b72= Te6edf3usbDevicesTb4b4b4,
Te6edf3discoveredTcpDevices Tff7b72= Te6edf3discoveredTcpDevicesTb4b4b4,
Te6edf3recentTcpDevices Tff7b72= Te6edf3recentTcpDevicesTb4b4b4,
Te6edf3isBleScanning Tff7b72= Te6edf3isBleScanningTb4b4b4,
Te6edf3isNetworkScanning Tff7b72= Te6edf3isNetworkScanningTb4b4b4,
Te6edf3activeTransport Tff7b72= Te6edf3activeTransportTb4b4b4,
Te6edf3onSelectDevice Tff7b72= Tb4b4b4{ Te6edf3entry Tff7b72-Tff7b72>
T8b949e// Recent TCP addresses are persisted, so this list renders without a scan β and
T8b949e// therefore without the scan toggle's permission request ever having run. BLE and
T8b949e// USB are unaffected by ACCESS_LOCAL_NETWORK, so only gate Tcp.
Tff7b72if Tb4b4b4(Te6edf3entry Tff7b72is Te6edf3DeviceListEntryTb4b4b4.Te6edf3TcpTb4b4b4) Tb4b4b4{
Te6edf3gateTcpConnect Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3entryTb4b4b4) Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3scanModelTb4b4b4.Te6edf3onSelectedTb4b4b4(Te6edf3entryTb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3onToggleBleScan Tff7b72= Tb4b4b4{
Tff7b72when Tb4b4b4{
T8b949e// Always allow stopping an in-progress scan.
Te6edf3isBleScanning Tff7b72-Tff7b72> Te6edf3scanModelTb4b4b4.Te6edf3toggleBleScanTb4b4b4(Tb4b4b4)
T8b949e// Granted but the radio is off β scanning can't work, so open BT settings.
Te6edf3bluetoothPermissionTb4b4b4.Te6edf3isGranted Tff7b72&Tff7b72& Te6edf3bluetoothDisabled Tff7b72-Tff7b72> Te6edf3openBluetoothSettingsTb4b4b4(Tb4b4b4)
Te6edf3bluetoothPermissionTb4b4b4.Te6edf3isGranted Tff7b72-Tff7b72> Te6edf3scanModelTb4b4b4.Te6edf3toggleBleScanTb4b4b4(Tb4b4b4)
T8b949e// Permanently denied: the system won't prompt again, so send to settings.
Te6edf3bluetoothPermissionTb4b4b4.Te6edf3status Tff7b72=Tff7b72= Te6edf3PermissionStatusTb4b4b4.Te6edf3PERMANENTLY_DENIED Tff7b72-Tff7b72>
Te6edf3bluetoothPermissionTb4b4b4.Te6edf3openAppSettingsTb4b4b4(Tb4b4b4)
T8b949e// Request in-context; once granted the user can start scanning.
Tff7b72else Tff7b72-Tff7b72> Te6edf3bluetoothPermissionTb4b4b4.Te6edf3requestTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3onToggleNetworkScan Tff7b72= Tb4b4b4{
Tff7b72when Tb4b4b4{
Te6edf3isNetworkScanning Tff7b72|Tff7b72| Te6edf3localNetworkPermissionTb4b4b4.Te6edf3isGranted Tff7b72-Tff7b72>
Te6edf3scanModelTb4b4b4.Te6edf3toggleNetworkScanTb4b4b4(Tb4b4b4)
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3status Tff7b72=Tff7b72= Te6edf3PermissionStatusTb4b4b4.Te6edf3PERMANENTLY_DENIED Tff7b72-Tff7b72>
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3openAppSettingsTb4b4b4(Tb4b4b4)
Tff7b72else Tff7b72-Tff7b72> Tb4b4b4{
T8b949e// Prefer requesting the runtime grant over letting the platform fall back
T8b949e// to the system NSD picker. Persist the user's intent so that if they
T8b949e// grant after the prompt, the scan starts via the LifecycleStartEffect and
T8b949e// stays on for next session.
Te6edf3scanModelTb4b4b4.Te6edf3persistNetworkAutoScanIntentTb4b4b4(Tff7b72trueTb4b4b4)
Te6edf3localNetworkPermissionTb4b4b4.Te6edf3requestTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3onAddManualAddress Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3fullAddress Tff7b72-Tff7b72>
T8b949e// Typing an address is always allowed β the target may be a public host or VPN
T8b949e// peer. The gate runs at connect, where the permission can actually matter.
Te6edf3gateTcpConnect Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3connectToManualAddressTb4b4b4(Te6edf3fullAddressTb4b4b4) Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Te6edf3onRemoveRecentAddress Tff7b72= Tb4b4b4{ Te6edf3scanModelTb4b4b4.Te6edf3removeRecentAddressTb4b4b4(Tffa657itTb4b4b4.Te6edf3fullAddressTb4b4b4) Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/** Informational, non-dismissible nudge for a connected device with a newer stable firmware release. */
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffFirmwareUpdateNoticeCardTb4b4b4(Te6edf3noticeTb4b4b4: Te6edf3FirmwareUpdateNoticeTb4b4b4, Te6edf3onActionTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3actionLabel Tff7b72=
Te6edf3stringResourceTb4b4b4(
Tff7b72when Tb4b4b4(Te6edf3noticeTb4b4b4.Te6edf3destinationTb4b4b4) Tb4b4b4{
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3AndroidUpdate Tff7b72-Tff7b72> Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_update_open
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3MeshtasticFlasher Tff7b72-Tff7b72> Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_update_open_flasher
Tb4b4b4}Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3message Tff7b72=
Te6edf3stringResourceTb4b4b4(
Tff7b72when Tb4b4b4(Te6edf3noticeTb4b4b4.Te6edf3destinationTb4b4b4) Tb4b4b4{
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3AndroidUpdate Tff7b72-Tff7b72> Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_update_notification_android
Te6edf3FirmwareUpdateDestinationTb4b4b4.Te6edf3MeshtasticFlasher Tff7b72-Tff7b72> Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_update_notification_flasher
Tb4b4b4}Tb4b4b4,
Te6edf3noticeTb4b4b4.Te6edf3currentVersionTb4b4b4,
Te6edf3noticeTb4b4b4.Te6edf3stableVersionTb4b4b4,
Tb4b4b4)
Te6edf3CardTb4b4b4(
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3colors Tff7b72= Te6edf3CardDefaultsTb4b4b4.Te6edf3cardColorsTb4b4b4(Te6edf3containerColor Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3surfaceContainerHighTb4b4b4)Tb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3RowTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3paddingTb4b4b4(T79c0ff1T79c0ff6.Te6edf3dpTb4b4b4)Tb4b4b4, Te6edf3verticalAlignment Tff7b72= Te6edf3AlignmentTb4b4b4.Te6edf3TopTb4b4b4) Tb4b4b4{
Te6edf3IconTb4b4b4(
Te6edf3imageVector Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3SystemUpdateTb4b4b4,
Te6edf3contentDescription Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3tint Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3primaryTb4b4b4,
Tb4b4b4)
Te6edf3ColumnTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3weightTb4b4b4(T79c0ff1fTb4b4b4)Tb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3start Tff7b72= T79c0ff1T79c0ff2.Te6edf3dpTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3TextTb4b4b4(
Te6edf3text Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3firmware_update_availableTb4b4b4)Tb4b4b4,
Te6edf3style Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3typographyTb4b4b4.Te6edf3titleMediumTb4b4b4,
Tb4b4b4)
Te6edf3TextTb4b4b4(
Te6edf3text Tff7b72= Te6edf3messageTb4b4b4,
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3top Tff7b72= T79c0ff4.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3style Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3typographyTb4b4b4.Te6edf3bodyMediumTb4b4b4,
Te6edf3color Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3onSurfaceVariantTb4b4b4,
Tb4b4b4)
Te6edf3ButtonTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3top Tff7b72= T79c0ff1T79c0ff2.Te6edf3dpTb4b4b4)Tb4b4b4, Te6edf3onClick Tff7b72= Te6edf3onActionTb4b4b4) Tb4b4b4{
Te6edf3IconTb4b4b4(Te6edf3imageVector Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3SystemUpdateTb4b4b4, Te6edf3contentDescription Tff7b72= Tff7b72nullTb4b4b4)
Te6edf3SpacerTb4b4b4(Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3widthTb4b4b4(T79c0ff8.Te6edf3dpTb4b4b4)Tb4b4b4)
Te6edf3TextTb4b4b4(Te6edf3actionLabelTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e/** Body for the CONNECTED state β sits inside the shared outer Card in [ConnectionsScreen]. */
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffConnectedDeviceContentTb4b4b4(
Te6edf3ourNodeTb4b4b4: Te6edf3orgTb4b4b4.Te6edf3meshtasticTb4b4b4.Te6edf3coreTb4b4b4.Te6edf3modelTb4b4b4.Te6edf3NodeTff7b72?Tb4b4b4,
Te6edf3selectedDeviceTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3bleDevicesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tb4b4b4,
Te6edf3textTb4b4b4: Te6edf3CurrentlyConnectedTextTb4b4b4,
Te6edf3onNavigateToNodeDetailsTb4b4b4: Tb4b4b4(Tffa657IntTb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Te6edf3onClickDisconnectTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3ourNodeTff7b72?.Te6edf3let Tb4b4b4{ Te6edf3node Tff7b72-Tff7b72>
Te6edf3CurrentlyConnectedInfoTb4b4b4(
Te6edf3node Tff7b72= Te6edf3nodeTb4b4b4,
Te6edf3text Tff7b72= Te6edf3textTb4b4b4,
Te6edf3bleDevice Tff7b72= Te6edf3bleDevicesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedDevice Tb4b4b4} Tff7b72as Te6edf3DeviceListEntryTb4b4b4.Te6edf3BleTff7b72?Tb4b4b4,
Te6edf3onNavigateToNodeDetails Tff7b72= Te6edf3onNavigateToNodeDetailsTb4b4b4,
Te6edf3onClickDisconnect Tff7b72= Te6edf3onClickDisconnectTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Body for the CONNECTING state β sits inside the shared outer Card in [ConnectionsScreen]. */
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffConnectingDeviceContentTb4b4b4(
Te6edf3selectedDeviceTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3persistedDeviceNameTb4b4b4: Tffa657String?Tb4b4b4,
Te6edf3bleDevicesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tb4b4b4,
Te6edf3discoveredTcpDevicesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tb4b4b4,
Te6edf3recentTcpDevicesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tb4b4b4,
Te6edf3usbDevicesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceListEntryTff7b72>Tb4b4b4,
Te6edf3connectionStatusTb4b4b4: Te6edf3ConnectionStatusTb4b4b4,
Te6edf3connectionProgressTb4b4b4: Tffa657String?Tb4b4b4,
Te6edf3onClickDisconnectTb4b4b4: Tb4b4b4(Tb4b4b4) Tff7b72-Tff7b72> Tffa657UnitTb4b4b4,
Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3selectedEntry Tff7b72=
Te6edf3bleDevicesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedDevice Tb4b4b4}
Tff7b72?: Te6edf3discoveredTcpDevicesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedDevice Tb4b4b4}
Tff7b72?: Te6edf3recentTcpDevicesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedDevice Tb4b4b4}
Tff7b72?: Te6edf3usbDevicesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fullAddress Tff7b72=Tff7b72= Te6edf3selectedDevice Tb4b4b4}
T8b949e// Use the entry name if found in scan lists, otherwise fall back to the persisted name
T8b949e// from the last successful selection, and only show "Unknown Device" as a last resort.
Tff7b72val Te6edf3name Tff7b72= Te6edf3selectedEntryTff7b72?.Te6edf3name Tff7b72?: Te6edf3persistedDeviceName Tff7b72?: Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3unknown_deviceTb4b4b4)
Tff7b72val Te6edf3address Tff7b72= Te6edf3selectedEntryTff7b72?.Te6edf3address Tff7b72?: Te6edf3selectedDevice
Te6edf3ConnectingDeviceInfoTb4b4b4(
Te6edf3deviceName Tff7b72= Te6edf3nameTb4b4b4,
Te6edf3deviceAddress Tff7b72= Te6edf3addressTb4b4b4,
Te6edf3connectionStatus Tff7b72= Te6edf3connectionStatusTb4b4b4,
Te6edf3connectionProgress Tff7b72= Te6edf3connectionProgressTb4b4b4,
Te6edf3onClickDisconnect Tff7b72= Te6edf3onClickDisconnectTb4b4b4,
Tb4b4b4)
Tb4b4b4}
T8b949e/** Body for the NO_DEVICE state β sits inside the shared outer Card in [ConnectionsScreen]. */
Tf0883e@Composable
Tff7b72private Tff7b72fun Td2a8ffNoDeviceContentTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3ColumnTb4b4b4(
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3fillMaxWidthTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3paddingTb4b4b4(T79c0ff1T79c0ff6.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3verticalArrangement Tff7b72= Te6edf3ArrangementTb4b4b4.Te6edf3CenterTb4b4b4,
Te6edf3horizontalAlignment Tff7b72= Te6edf3AlignmentTb4b4b4.Te6edf3CenterHorizontallyTb4b4b4,
Tb4b4b4) Tb4b4b4{
Te6edf3IconTb4b4b4(
Te6edf3imageVector Tff7b72= Te6edf3MeshtasticIconsTb4b4b4.Te6edf3NoDeviceTb4b4b4,
Te6edf3contentDescription Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3sizeTb4b4b4(T79c0ff6T79c0ff4.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3tint Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3outlineVariantTb4b4b4,
Tb4b4b4)
Te6edf3TextTb4b4b4(
Te6edf3text Tff7b72= Te6edf3stringResourceTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3no_device_selectedTb4b4b4)Tb4b4b4,
Te6edf3modifier Tff7b72= Te6edf3ModifierTb4b4b4.Te6edf3paddingTb4b4b4(Te6edf3top Tff7b72= T79c0ff1T79c0ff6.Te6edf3dpTb4b4b4)Tb4b4b4,
Te6edf3style Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3typographyTb4b4b4.Te6edf3bodyLargeTb4b4b4,
Te6edf3textAlign Tff7b72= Te6edf3TextAlignTb4b4b4.Te6edf3CenterTb4b4b4,
Te6edf3color Tff7b72= Te6edf3MaterialThemeTb4b4b4.Te6edf3colorSchemeTb4b4b4.Te6edf3outlineTb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
T8b949e/** Visual state for the connection screen's [AnimatedContent] transition between the three card body variants. */
Tff7b72private Tff7b72enum Tff7b72class T56d364ConnectionUiState Tb4b4b4{
T8b949e/** No device is selected. */
Te6edf3NO_DEVICETb4b4b4,
T8b949e/** A device is selected or we are actively connecting. */
Te6edf3CONNECTINGTb4b4b4,
T8b949e/** Connected with node info available. */
Te6edf3CONNECTED_WITH_NODETb4b4b4,
Tb4b4b4}
Served by rngit 1.5.0 - Generated in 0.15s